%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "GoF taxonomy of design patterns"
%%| fig-width: 6.2
%%| fig-height: 3.2
flowchart TB
Patterns["Design Patterns"]
C["Creational"]
S["Structural"]
B["Behavioral"]
Patterns --> C
Patterns --> S
Patterns --> B
W9. Design Patterns: Singleton, State, Prototype, Builder
1. Summary
1.1 Introduction to Design Patterns
1.1.1 What Is a Design Pattern?
A design pattern is an architectural scheme — a certain organization of classes, objects, and methods — that provides applications with a standardized, reusable solution to a recurring design problem. The concept was popularized by the so-called “Gang of Four” (GoF): Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides in their landmark 1994 book Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley).
The defining description from the GoF: “Each pattern describes a problem that occurs over and over again in our environment, and then describes the core of the solution to that problem in such a way that you can use this solution a million times over, without ever doing it the same way twice.”
Since 1994, various books have catalogued important patterns. Other well-known references include Design Patterns Explained by Alan Shalloway and James R. Trott, and language-specific books such as Design Patterns in Java by Steven John Metsker and William C. Wake, and Design Patterns in C# by Steven John Metsker.
1.1.2 Why Patterns Matter
“Designing object-oriented software is hard and designing reusable object-oriented software is even harder.” — Erich Gamma
Experienced object-oriented designers consistently produce better designs than novices. The reason is that experts have internalized recurring patterns of classes and objects that appear again and again across different applications. Design patterns make these patterns explicit and transferable:
- They solve specific design problems and make OO designs more flexible, elegant, and ultimately reusable.
- Without patterns, novice programmers often reinvent the wheel — or write fragile, hard-to-extend code.
- Patterns provide a shared vocabulary: saying “use a Singleton here” communicates an entire architectural intent in one word.
All design patterns exploit the OOP paradigm — the patterns are almost completely about OOP.
1.1.3 Taxonomy of Design Patterns
The GoF classified 23 patterns into three families based on their purpose:
- Creational Patterns — deal with the best way to create instances of objects. They abstract the instantiation process, making it easier to introduce new kinds of objects or control how many instances exist. Examples: Abstract Factory, Factory Method, Singleton, Builder, Prototype.
- Structural Patterns — describe how classes and objects can be combined to form larger, more complex structures. Examples: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy.
- Behavioral Patterns — are concerned with the assignment of responsibilities between objects, and with encapsulating behavior in an object and delegating requests to it. Examples: Chain of Responsibility, Command (undo/redo), Interpreter, Observer, Iterator, State, Mediator, Memento, Strategy, Template Method, Visitor.
Important note: There is no strong formal theory behind design patterns. Rather, patterns summarize vast practical experience from real-world OOP applications. They are empirical, not axiomatic.
In this week’s material, we cover three creational patterns — Singleton, Prototype, and Builder — and one behavioral pattern: State.
1.2 Singleton
1.2.1 Motivation: Why One Instance?
Some resources in a program must exist in exactly one copy throughout the entire lifetime of the application. Consider:
- A cache file — you do not want two independent caches with inconsistent data.
- A file with virtual memory pages in an OS or VM — managed globally.
- Certain dialog windows in a GUI — only one “Preferences” dialog should exist at a time.
- Device drivers — a single driver per device.
- Logger classes, Configuration classes, Access to shared resources — all need a unique global point of access.
Why not to use a global variable (or a static variable)?
- Uncontrolled access: Any code can read or overwrite the variable at any time.
- Cannot control creation time: A global variable is initialized at program start, regardless of whether it is needed. This wastes resources and complicates initialization order.
1.2.2 Building the Singleton Step by Step
The core problem: “How do we guarantee that a class can be instantiated exactly once?”
Attempt 1: A plain class — new myClass() can be called as many times as you like. No restriction at all.
Attempt 2: Make the constructor private.
class myClass {
private myClass() { }
}Now no external code can call new myClass(). But this goes too far — now nobody can create an instance, not even the class itself.
Attempt 3: Provide a static factory method.
class myClass {
private myClass() { }
public static myClass getInstance() {
return new myClass(); // still creates a new one every time!
}
}The constructor is private, but getInstance() calls new every time — you still get multiple instances. What should we add to make the uniqueness of the instance created?
The complete solution — add a private static field that stores the one-and-only instance, and check it before creating:
public class Singleton {
// Step 1: private static field — holds the single instance (null at start)
private static Singleton unique;
// Step 4: private constructor — no external code can instantiate this class
private Singleton() { }
// Steps 2 & 3: public static factory method with lazy initialization
public static Singleton getInstance() {
if (unique == null) { // first call: instance doesn't exist yet
unique = new Singleton(); // create the one and only instance
}
return unique; // all calls return the same instance
}
}%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Singleton structure: one private instance and one public static access point"
%%| fig-width: 5.8
%%| fig-height: 3
classDiagram
class Singleton {
- static unique
- Singleton()
+ static getInstance()
}
The private static member keeps the reference to the single instance, initialized to null at program start. After the very first call to getInstance, unique holds the reference forever. There is no other way to get access to unique except via a call to getInstance.
The five implementation steps are:
- Add a private static field to the class for storing the singleton instance.
- Declare a public static creation method for getting the singleton instance.
- Implement lazy initialization inside the static method: create a new object on its first call and store it in the static field. All subsequent calls return the same instance.
- Make the constructor private so that no external code can directly call
new. - In client code, replace all direct constructor calls with calls to the static creation method.
1.2.3 Lazy vs. Non-Lazy Initialization
Lazy Singleton (preferred): The instance is created on demand — only when getInstance() is first called.
public class LazySingleton {
private static LazySingleton unique;
private LazySingleton() { }
public static LazySingleton getInstance() {
if (unique == null) {
unique = new LazySingleton(); // lazy initialization
}
return unique;
}
}Non-Lazy Singleton (avoid): The instance is created immediately when the class is loaded, regardless of whether it is ever used.
// Better to avoid this implementation
public class NonLazySingleton {
private static final NonLazySingleton unique = new NonLazySingleton();
private NonLazySingleton() { }
public static NonLazySingleton getInstance() {
return unique;
}
}The non-lazy approach wastes resources if the singleton is never used. The lazy approach is generally preferred.
1.2.4 C++ Singleton
In C++, the static field must be defined outside the class body in addition to being declared inside it:
class Singleton {
private:
static Singleton* instance;
Singleton() {} // Private constructor
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
// Required: define the static member outside the class
Singleton* Singleton::instance = nullptr;1.2.5 Thread Safety Concern
The basic Singleton implementation is not thread-safe. In a multithreaded environment, two threads could both reach the if (unique == null) check simultaneously, both see null, and both create new instances — breaking the single-instance guarantee.
Note: If you’re creating an application with multithreading support, you should place a thread lock inside
getInstance()before creating the instance.
An industrial-strength Singleton requires synchronization, for example using double-checked locking with a volatile field in Java (see Examples section).
1.2.6 Is Singleton an Anti-Pattern?
No, the Singleton design pattern is not considered an anti-pattern. However, it can be overused and misused. It should be used judiciously and with care, taking into account the specific requirements and constraints of the application.
1.2.7 Pros and Cons
Pros:
- You can be sure that a class has only a single instance.
- You gain a global access point to that instance.
- The singleton object is initialized only when it is first requested (lazy initialization).
Cons:
- Violates the Single Responsibility Principle: the pattern solves two problems at the same time (business logic + instance management).
- Can mask bad design — when components know too much about each other.
- Requires special treatment in multithreaded environments to prevent race conditions.
- Difficult to unit test: many test frameworks rely on inheritance when producing mock objects. Since the constructor is private and overriding static methods is impossible in most languages, mocking the Singleton is very hard.
1.3 State
1.3.1 What Is the State Pattern?
The State pattern allows an object to alter its behavior when its internal state changes. From the outside, the object appears to change its class — the same method call produces different results depending on the current state.
The conceptual foundation is the finite state machine (FSM): a virtual system that can be in exactly one of a finite number of states at any moment. When an action is performed, the machine transitions to a new state.
1.3.2 Motivating Example: Lexical Analyzer
A lexical analyzer (the first phase of a compiler) reads source code character by character and groups characters into tokens — minimal language units that have a concrete meaning (identifiers, integer literals, delimiters, operator signs, etc.).
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Scanner state machine for identifiers and integers"
%%| fig-width: 6.4
%%| fig-height: 3.8
stateDiagram-v2
[*] --> S
S --> Id: letter
S --> Int: digit
Id --> Id: letter or digit
Id --> IdDone: other
Int --> Int: digit
Int --> IntDone: other
The scanner must track what kind of token it is currently reading. These are its states for scanning identifiers and integers:
- S (Initial state): prepare the buffer for an identifier or an integer.
- State 1 (Reading identifier): a letter was seen; keep reading letters and digits. Actions: add letter to buffer; add digit or letter to buffer.
- State 3 (Identifier complete): a non-alphanumeric character was seen after letters — the identifier is done; add it to the symbol table.
- State 4 (Reading integer): a digit was seen; keep reading digits. Action: add digit to buffer.
- State 5 (Integer complete): a non-digit was seen after digits — the integer constant is done; convert it to binary form.
Transitions: from S, a letter → State 1; a digit → State 4. From State 1, letter or digit → stay in 1; other → State 3. From State 4, digit → stay in 4; other → State 5. This FSM structure maps directly to the State design pattern.
1.3.3 Another Example: States of Water
Imagine a device that controls actions performed on water. Water has states — Solid (ice), Liquid, Gas — and three actions trigger transitions: Heating, Freezing, Cooling. There is also a fourth state: High-temp. Gas.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Water state transitions"
%%| fig-width: 6.2
%%| fig-height: 3.6
stateDiagram-v2
Solid --> Liquid: heating
Liquid --> Gas: heating
Gas --> HighTempGas: heating
HighTempGas --> Gas: cooling
Gas --> Liquid: cooling
Liquid --> Solid: freezing
Straightforward implementation (the bad way):
The naive approach stores the state as an enum and uses if/else if chains inside each action method:
enum WaterState { SOLID, LIQUID, GAS }
class Water {
// C# auto-property: { get; set; } is shorthand for a property
// with an auto-generated private backing field.
// Equivalent to declaring a private field + public getter + public setter.
public WaterState State { get; set; }
public Water(WaterState ws) { State = ws; }
public void Heating() {
if (State == WaterState.SOLID)
State = WaterState.LIQUID; // ice to liquid
else if (State == WaterState.LIQUID)
State = WaterState.GAS; // liquid to gas
else if (State == WaterState.GAS)
{ /* increasing temperature */ }
}
public void Freezing() {
if (State == WaterState.LIQUID)
State = WaterState.SOLID; // liquid to ice
else if (State == WaterState.GAS)
State = WaterState.LIQUID; // gas to liquid
}
public void Cooling() { /* ... */ }
}Why this is bad:
- States and actions are separated: to add a new state (e.g., “High-temp. Gas”), you must update every action method.
- Each action method contains a growing
ifladder — hard to read and error-prone.
Solution: treat a state as an object with functionality.
1.3.4 Applying the State Pattern to Water
Step 1 — Replace the enum with an interface:
Instead of enum WaterState, declare an interface WaterState where every concrete state class knows how to handle each action:
// Old: enum WaterState { SOLID, LIQUID, GAS }
// New: interface — each state object handles actions itself
interface WaterState {
void Heating(Water water);
void Freezing(Water water);
void Cooling(Water water);
}
class SolidWater : WaterState { ... }
class LiquidWater : WaterState { ... }
class GasWater : WaterState { ... }Step 2 — Redesign the Water class:
The Water class no longer contains any if statements. Each action simply delegates to the current state object:
class Water {
public WaterState State { get; set; }
public Water(WaterState ws) { State = ws; }
// Each action redirects control to the current state object.
// Note: there are NO if-statements here.
public void Heating() { State.Heating(this); }
public void Freezing() { State.Freezing(this); }
public void Cooling() { State.Cooling(this); }
}Step 3 — Implement the concrete state classes:
Each state class encapsulates the transition logic from that state. The state object changes the context’s (Water’s) state by assigning a new state object:
class LiquidWater : WaterState {
public void Heating(Water water) {
water.State = new GasWater(); // liquid → gas
}
public void Freezing(Water water) {
water.State = new SolidWater(); // liquid → ice
}
public void Cooling(Water water) {
// cooling liquid — no change
}
}
class GasWater : WaterState {
public void Heating(Water water) { /* increase temperature */ }
public void Freezing(Water water) { water.State = new LiquidWater(); }
public void Cooling(Water water) { water.State = new LiquidWater(); }
}
class SolidWater : WaterState {
public void Heating(Water water) { water.State = new LiquidWater(); }
public void Freezing(Water water) { /* already solid */ }
public void Cooling(Water water) { /* already solid */ }
}Step 4 — Adding a new state is trivial:
To add “High-temp. Gas”, simply add a new class — no existing class needs to change:
class HTGasWater : WaterState {
public void Heating(Water water) { /* heating — no change */ }
public void Freezing(Water water) { water.State = new GasWater(); }
public void Cooling(Water water) { water.State = new GasWater(); }
}%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "State pattern structure: context delegates behavior to the current state object"
%%| fig-width: 6.2
%%| fig-height: 3.2
classDiagram
class Context
class State {
<<interface>>
+handle()
}
class ConcreteStateA
class ConcreteStateB
Context --> State : currentState
State <|.. ConcreteStateA
State <|.. ConcreteStateB
1.3.5 State Pattern in C++ (Document Example)
Here is a C++ illustration using a Document that can be in Draft or Published state:
class State {
public:
virtual void review() = 0;
virtual ~State() = default;
};
class Draft : public State {
public:
void review() override {
std::cout << "Draft: Reviewing changes\n";
}
};
class Published : public State {
public:
void review() override {
std::cout << "Published: Review not allowed\n";
}
};
class Document {
private:
std::unique_ptr<State> state;
public:
Document(std::unique_ptr<State> initialState)
: state(std::move(initialState)) { }
void setState(std::unique_ptr<State> newState) {
state = std::move(newState);
}
void review() {
state->review(); // delegates to current state
}
};1.3.6 Advantages of the State Pattern
- Encapsulation: The behavior for each state is encapsulated in its own class — changing a state’s behavior means changing exactly one class.
- Easy to extend: Adding a new state means adding a new class without modifying any existing class (Open/Closed Principle).
- No
if-statements: The context object is clean and concise; all branching logic lives in the state hierarchy. - How to update the behavior of a state: Just change the corresponding class.
1.4 Prototype
1.4.1 What Is the Prototype Pattern?
The Prototype pattern is a creational pattern that creates new objects by cloning (copying) an existing object — the prototype — rather than constructing them from scratch. The prototype “knows” how to copy itself.
When to use Prototype:
- If the type of object being created should be determined dynamically at runtime.
- If simple technique is enough (will see Abstract Factory for similar purposes later).
- If cloning is more preferable than creating by
newand launching a constructor — for example, when you want to copy an already-configured object rather than reconfigure a fresh one. - When your code should not depend on the concrete class of the object being copied — you only need the abstract
clone()interface. - When you want to reduce the number of subclasses that only differ in the way they initialize their respective objects.
1.4.2 Motivating Example: Geometric Figures
Imagine you are working with geometric figures dynamically. Instead of creating each figure “from scratch” with all its attributes, you create a copy of an existing figure that already has the desired attributes — a clone:
interface iFigure {
// Common interface for cloning: hides the concrete cloning algorithm
iFigure Clone();
void Display();
}The concrete classes implement Clone() by calling their own constructor with the current object’s values:
class Rectangle : iFigure {
int width, height;
public Rectangle(int w, int h) { width = w; height = h; }
public iFigure Clone() {
// Clone method encapsulates the real cloning algorithm:
// it might use 'new', or system tools like MemberwiseClone() in .NET
return new Rectangle(this.width, this.height);
}
public void Display() { /* ... */ }
}
class Circle : iFigure {
int radius;
public Circle(int r) { radius = r; }
public iFigure Clone() {
return new Circle(this.radius);
}
public void Display() { /* ... */ }
}The client calls figure.Clone() without knowing whether figure is a Rectangle, Circle, or some other shape. The Clone() call dispatches polymorphically:
iFigure figure, clone;
figure = new Rectangle(30, 40);
clone = figure.Clone(); // Clone method call creates the copy of Rectangle
figure = new Circle(30);
clone = figure.Clone(); // The same call creates the copy of CircleNote on .NET: In C#/.NET,
MemberwiseClone()is a built-in method inherited fromSystem.Objectthat performs a shallow copy of an object (copies all fields, but does not deep-copy reference-type fields). TheClone()method can use it as an alternative to a manually written copy constructor for simple objects.
1.4.3 Prototype Structure
The canonical structure involves:
- A Prototype interface (or abstract class) declaring
clone(): Prototype. - ConcretePrototype classes with a copy constructor
ConcretePrototype(prototype)that copies all fields:this.field1 = prototype.field1. Theclone()method is simply:return new ConcretePrototype(this). - SubclassPrototype classes that call
super(prototype)first to copy parent fields, then copy their own:this.field2 = prototype.field2. Theirclone()returnsnew SubclassPrototype(this).
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Prototype pattern: clone creates new objects from existing exemplars"
%%| fig-width: 6.2
%%| fig-height: 3
classDiagram
class Prototype {
<<interface>>
+clone()
}
class ConcretePrototype
class SubclassPrototype
Prototype <|.. ConcretePrototype
ConcretePrototype <|-- SubclassPrototype
1.4.4 Java Implementation: Shape Hierarchy
Here is a complete Java example where an abstract Shape base class provides a copy constructor, and subclasses (Circle, Rectangle) inherit it:
public abstract class Shape {
private int x, y;
private String color;
public Shape() { }
// Copy constructor: copies base-class fields from the given target
public Shape(Shape target) {
if (target != null) {
this.x = target.x;
this.y = target.y;
this.color = target.color;
}
}
// Every concrete subclass must implement clone()
public abstract Shape clone();
@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Shape)) return false;
Shape shape2 = (Shape) object2;
return shape2.x == x && shape2.y == y
&& Objects.equals(shape2.color, color);
}
// getters / setters omitted for brevity
}public class Circle extends Shape {
private int radius;
public Circle() { }
// Copy constructor: call super to copy base fields, then copy own fields
public Circle(Circle target) {
super(target);
if (target != null) this.radius = target.radius;
}
@Override
public Shape clone() {
return new Circle(this); // uses copy constructor
}
@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Circle) || !super.equals(object2)) return false;
return ((Circle) object2).radius == radius;
}
public int getRadius() { return radius; }
public void setRadius(int radius) { this.radius = radius; }
}public class Rectangle extends Shape {
private int width, height;
public Rectangle() { }
public Rectangle(Rectangle target) {
super(target);
if (target != null) {
this.width = target.width;
this.height = target.height;
}
}
@Override
public Shape clone() { return new Rectangle(this); }
@Override
public boolean equals(Object object2) {
if (!(object2 instanceof Rectangle) || !super.equals(object2)) return false;
Rectangle shape2 = (Rectangle) object2;
return shape2.width == width && shape2.height == height;
}
// getters / setters omitted
}Demo: cloning a mixed list of shapes
public class Demo {
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
List<Shape> shapesCopy = new ArrayList<>();
Circle circle = new Circle();
circle.setX(10); circle.setY(20);
circle.setRadius(15); circle.setColor("red");
shapes.add(circle);
Circle anotherCircle = (Circle) circle.clone(); // exact copy
shapes.add(anotherCircle);
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10); rectangle.setHeight(20);
rectangle.setColor("blue");
shapes.add(rectangle);
cloneAndCompare(shapes, shapesCopy);
}
private static void cloneAndCompare(List<Shape> shapes, List<Shape> shapesCopy) {
for (Shape shape : shapes) {
shapesCopy.add(shape.clone());
}
for (int i = 0; i < shapes.size(); i++) {
if (shapes.get(i) != shapesCopy.get(i)) {
System.out.println(i + ": Shapes are different objects (yay!)");
if (shapes.get(i).equals(shapesCopy.get(i)))
System.out.println(i + ": And their content are identical (yay!)");
else
System.out.println(i + ": But their content are not identical (booo!)");
} else {
System.out.println(i + ": Shape objects are the same (booo!)");
}
}
}
}1.4.5 C++ Prototype Example
class Car { // prototype interface
public:
virtual Car* clone() const = 0; // pure virtual clone
virtual void specs() const = 0;
virtual ~Car() {}
};
class Sedan : public Car { // concrete prototype
private:
std::string color;
public:
Sedan(const std::string& color) : color(color) {}
Car* clone() const override {
return new Sedan(*this); // copy constructor — copies all fields
}
void specs() const override {
std::cout << "Sedan Car - Color: " << color << std::endl;
}
};
int main() {
Car* originalCar = new Sedan("Red");
Car* clonedCar = originalCar->clone();
originalCar->specs(); // Sedan Car - Color: Red
clonedCar->specs(); // Sedan Car - Color: Red
delete originalCar;
delete clonedCar;
return 0;
}1.4.6 How to Implement Prototype (Step by Step)
- Create the prototype interface and declare the
clone()method. Or add it to an existing class hierarchy. - Each prototype class must define an alternative constructor (copy constructor) that accepts an object of the same class and copies all its fields. If subclassing, call
super(prototype)first so the parent handles its private fields. - The
clone()method typically consists of one line:return new ConcretePrototype(this). Every class must use its own class name withnew, otherwise the method may return a parent-class object. - Optionally, create a prototype registry — a centralized catalog mapping keys to frequently-used pre-configured prototypes.
1.4.7 Pros and Cons
Pros:
- Clone objects without coupling to their concrete classes — client only sees the
clone()interface. - Eliminate repeated initialization code — keep pre-configured prototypes and clone them.
- Produce complex objects more conveniently.
- Provides an alternative to inheritance when dealing with configuration presets.
Cons:
- Cloning objects with circular references can be tricky.
1.5 Builder
1.5.1 Motivation: The Telescoping Constructor Problem
Imagine a class House with many optional parameters: number of rooms, garage (yes/no), pool (yes/no), garden type, roof style, etc. The straightforward approach creates a telescoping constructor — a series of increasingly long constructors:
House(int rooms) { ... }
House(int rooms, boolean garage) { ... }
House(int rooms, boolean garage, boolean pool) { ... }
// ... unreadable and error-proneWhen a class has a large number of fields, creating constructors with many parameters makes the code difficult to read and maintain. This is called the telescoping constructor anti-pattern.
1.5.2 What Is the Builder Pattern?
The Builder pattern is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.
Key properties:
- Readable and maintainable code: Objects are built step-by-step with clearly named methods instead of a long parameter list.
- Flexibility: You can create objects step by step, setting only the fields that you need. This makes the code more adaptable to changing requirements.
- Enforces immutability: Once the object is fully constructed and returned by
build(), its fields are not modified further — the object’s state remains consistent throughout its lifetime.
When to use:
- To get rid of a “telescoping constructor”.
- When you want your code to create different representations of some product (for example, stone and wooden houses; classic and sports cars).
- To construct Composite trees or other complex objects.
1.5.3 Builder Structure
The pattern has four roles:
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Builder pattern roles"
%%| fig-width: 6.4
%%| fig-height: 3.2
classDiagram
class Director
class Builder {
<<interface>>
+reset()
+buildStepA()
+buildStepB()
+getResult()
}
class ConcreteBuilder
class Product
Director --> Builder : directs
Builder <|.. ConcreteBuilder
ConcreteBuilder --> Product : builds
- Product — the complex object being built (e.g.,
Car,PC,Document). - Builder interface — declares the construction steps (
reset(),buildStepA(),buildStepB(), …,buildStepZ()). Thereset()method clears the builder’s internal state so it can start building a fresh product. Each build step typically returns the builder itself to enable method chaining. - ConcreteBuilder — implements the Builder interface for a specific product representation (e.g.,
ClassicCarBuilder,SportsCarBuilder). Accumulates partial results in aresultfield and provides agetResult()orbuild()method that returns the finished product. - Director (optional) — knows which steps to call and in what order, and can conditionally select steps:
builder.reset(); if (type == "simple") { builder.buildStepA(); } else { builder.buildStepB(); builder.buildStepZ(); }. The Director encapsulates various ways to construct a product using the same builder object.
Typical client code:
b = new ConcreteBuilder1();
d = new Director(b);
d.make(type);
Product1 p = b.getResult();
1.5.4 C++ Builder Example: PC Builder
// Product
class PC {
std::string m_cpu, m_ram, m_storage;
public:
void setCPU(std::string cpu) { m_cpu = cpu; }
void setRAM(std::string ram) { m_ram = ram; }
void setStorage(std::string storage) { m_storage = storage; }
void showSpecs() { /* prints all fields */ }
};
// Abstract Builder
class PCBuilder {
public:
virtual ~PCBuilder() = default;
virtual void buildCPU() = 0;
virtual void buildRAM() = 0;
virtual void buildStorage() = 0;
virtual PC getResult() = 0;
};
// Concrete Builder
class GamingPCBuilder : public PCBuilder {
PC m_pc;
public:
GamingPCBuilder() { m_pc = PC(); }
void buildCPU() override { m_pc.setCPU("Intel i9-13900K"); }
void buildRAM() override { m_pc.setRAM("32GB DDR5"); }
void buildStorage() override { m_pc.setStorage("2TB NVMe SSD"); }
PC getResult() override { return m_pc; }
};
// Director
class Director {
PCBuilder* m_builder;
public:
void setBuilder(PCBuilder* builder) { m_builder = builder; }
PC construct() {
m_builder->buildCPU();
m_builder->buildRAM();
m_builder->buildStorage();
return m_builder->getResult();
}
};
// Client
int main() {
Director director;
GamingPCBuilder builder;
director.setBuilder(&builder);
PC pc = director.construct();
pc.showSpecs();
return 0;
}1.5.5 Java Builder Example: Car Builder with Method Chaining
This Java example demonstrates the fluent interface style — each step returns this so calls can be chained. It also shows two different ConcreteBuilder implementations (SportsCarBuilder and ClassicCarBuilder) producing different representations of the same product (Car):
// Builder interface — each step returns CarBuilder for method chaining
public interface CarBuilder {
CarBuilder fixChassis();
CarBuilder fixBody();
CarBuilder paint();
CarBuilder fixInterior();
Car build();
}public class SportsCarBuilder implements CarBuilder {
private String chassis, body, paint, interior;
@Override public CarBuilder fixChassis() { this.chassis = "Sporty Chassis"; return this; }
@Override public CarBuilder fixBody() { this.body = "Sporty Body"; return this; }
@Override public CarBuilder paint() { this.paint = "Sporty Torch Red Paint"; return this; }
@Override public CarBuilder fixInterior() { this.interior = "Sporty interior"; return this; }
@Override
public Car build() {
Car car = new Car(chassis, body, paint, interior);
if (car.doQualityCheck()) return car;
System.out.println("Car assembly is incomplete. Can't deliver!");
return null;
}
}public class ClassicCarBuilder implements CarBuilder {
private String chassis, body, paint, interior;
@Override public CarBuilder fixChassis() { this.chassis = "Classic Chassis"; return this; }
@Override public CarBuilder fixBody() { this.body = "Classic Body"; return this; }
@Override public CarBuilder paint() { this.paint = "Classic White Paint"; return this; }
@Override public CarBuilder fixInterior() { this.interior = "Classic interior"; return this; }
@Override
public Car build() {
Car car = new Car(chassis, body, paint, interior);
if (car.doQualityCheck()) return car;
System.out.println("Car assembly is incomplete. Can't deliver!");
return null;
}
}// Director — encapsulates the construction sequence
public class AutomotiveEngineer {
private CarBuilder builder;
public AutomotiveEngineer(CarBuilder builder) {
this.builder = builder;
if (this.builder == null)
throw new IllegalArgumentException(
"Automotive Engineer can't work without Car Builder!");
}
public Car manufactureCar() {
return builder.fixChassis()
.fixBody()
.paint()
.fixInterior()
.build();
}
}// Client code — swap out the builder to get a different car
public class Main {
public static void main(String[] args) {
CarBuilder builder = new SportsCarBuilder();
// CarBuilder builder = new ClassicCarBuilder(); // another option
AutomotiveEngineer engineer = new AutomotiveEngineer(builder);
Car car = engineer.manufactureCar();
if (car != null) {
System.out.println("Below car delivered:");
System.out.println("=".repeat(76));
System.out.println(car);
System.out.println("=".repeat(76));
} else {
System.out.println("Problems with current producing");
}
}
}Notice that Car.toString() itself uses Java’s StringBuilder — which is an application of the Builder pattern through the java.lang.Appendable interface.
1.5.6 How to Implement Builder (Step by Step)
- Make sure you can clearly define the common construction steps for all product representations. Otherwise, the pattern may not apply.
- Declare these steps in the base Builder interface.
- Create a ConcreteBuilder class for each product representation and implement all construction steps.
- Think about creating a Director class. It may encapsulate various ways to construct a product using the same builder object.
- The client creates both the builder and the director. Before construction starts, the client must pass a builder object to the director — usually once, via the director’s constructor. There’s an alternative approach where the builder is passed to a specific product construction method of the director.
- The construction result can be obtained directly from the director only if all products follow the same interface. Otherwise, the client should fetch the result directly from the builder via
getResult()orbuild().
1.5.7 Pros and Cons
Pros:
- Construct objects step by step, defer steps, or run steps recursively.
- Reuse the same construction code for different product representations.
- Single Responsibility Principle: complex construction logic is isolated from the product’s business logic.
- Enables method chaining for highly readable client code.
Cons:
- Increased complexity: the pattern requires multiple new classes (Builder interface + concrete builders + optional Director).
1.6 Comparing Builder and Prototype
Both Builder and Prototype are creational patterns, but they solve different problems:
| Prototype | Builder | |
|---|---|---|
| Idea | Copy an existing object | Assemble a new object step by step |
| Input | An existing configured prototype | A sequence of construction calls |
| Use when | Objects are expensive to configure from scratch | Objects have many optional parts or representations |
| Key mechanism | clone() / copy constructor |
buildStepX() + build() / getResult() |
2. Definitions
- Design Pattern: An architectural scheme — a standardized organization of classes, objects, and methods — providing a reusable solution to a recurring OOP design problem.
- Gang of Four (GoF): Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — authors of the seminal 1994 book Design Patterns, which catalogued 23 fundamental patterns.
- Creational Patterns: Design patterns that abstract the object creation process (Singleton, Prototype, Builder, Factory Method, Abstract Factory).
- Structural Patterns: Design patterns that describe how to compose classes and objects into larger structures (Adapter, Composite, Facade, etc.).
- Behavioral Patterns: Design patterns that define how objects communicate and distribute responsibility (State, Observer, Strategy, etc.).
- Singleton: A creational pattern that ensures a class has exactly one instance, accessible via a global static method (
getInstance()). - Lazy Initialization: Creating an object only when it is first requested, rather than at program startup. Used in the Singleton pattern to save resources.
- Non-Lazy (Eager) Initialization: Creating an object immediately when its class is loaded, regardless of whether it is needed.
- Thread Safety: The property of code that guarantees correct behavior when executed by multiple threads simultaneously. The basic Singleton is not thread-safe without synchronization.
- State Pattern: A behavioral pattern where an object’s behavior changes based on its internal state, implemented by delegating actions to a state object rather than using
if/elsechains. - Finite State Machine (FSM): A model of computation with a finite number of states, one active at a time, and transitions triggered by actions or inputs.
- Token: In a lexical analyzer, a minimal language unit with a concrete meaning (identifier, integer literal, operator, delimiter, etc.).
- C# Auto-Property: The
{ get; set; }syntax in C#, which automatically generates a private backing field and public getter/setter for a property — shorthand for writing them explicitly. - Prototype Pattern: A creational pattern that creates new objects by cloning (copying) an existing prototype, rather than constructing them from scratch.
- Clone: A copy of an object produced by calling the
clone()method — a separate object with the same field values. - Copy Constructor: A constructor that accepts an object of the same class and initializes the new object by copying all fields from it.
- Shallow Copy: A copy where all fields are copied by value — reference-type fields in the copy point to the same objects as in the original.
MemberwiseClone()in .NET performs a shallow copy. - MemberwiseClone(): A built-in .NET method inherited from
System.Objectthat creates a shallow copy of the current object — an alternative to a manually written copy constructor for simple classes. - Prototype Registry: An optional component — a dictionary mapping keys to pre-configured prototype objects for easy retrieval and cloning.
- Builder Pattern: A creational pattern that separates the construction of a complex object from its representation, allowing the same construction process to produce different results.
- Director: An optional class in the Builder pattern that encapsulates a specific construction sequence, calling the builder’s steps in the correct order and combination.
- Telescoping Constructor Anti-Pattern: The problematic practice of defining many overloaded constructors with increasing numbers of parameters to handle optional fields — solved by the Builder pattern.
- Method Chaining (Fluent Interface): A programming style where each builder step returns
this, enabling call chains likebuilder.stepA().stepB().build(). - reset(): A method in the Builder interface that clears the builder’s internal state so a new product can be constructed from scratch.
3. Examples
3.1. Lecture Recap: Design Patterns Review (Lab 8, Task 1)
Answer the following recap questions:
(a) What are design patterns? Why do we need them?
(b) What are the three types of design patterns? How are they different?
(c) Give three examples for each design pattern type.
(d) Which design pattern prohibits the creation of multiple objects from a class?
(e) True or False: The State pattern encapsulates the behavior for each state in an object.
(f) When to use the Prototype pattern, and what advantage can it provide?
(g) What are the differences between the Builder and the Prototype patterns?
Click to see the solution
(a) What are design patterns? Why do we need them?
A design pattern is an architectural scheme — a standardized organization of classes, objects, and methods — that provides a reusable solution to a recurring design problem in OOP.
We need them because:
- Experienced designers produce better designs by recognizing and applying recurring patterns.
- They provide a shared vocabulary for communicating design decisions.
- They make OO software more flexible, elegant, and reusable.
- Without them, developers repeatedly solve the same problems from scratch, often poorly.
(b) Three types of design patterns and their differences:
- Creational — deal with the best way to create instances of objects. They abstract the instantiation process.
- Structural — describe how classes and objects can be combined to form larger structures (composition, adaptation, wrapping).
- Behavioral — concerned with the assignment of responsibilities between objects; encapsulating behavior and delegating requests.
(c) Three examples for each type:
- Creational: Singleton, Builder, Prototype (also: Abstract Factory, Factory Method)
- Structural: Adapter, Composite, Facade (also: Bridge, Decorator, Flyweight, Proxy)
- Behavioral: State, Observer, Strategy (also: Command, Iterator, Chain of Responsibility, Memento, Template Method, Visitor, Mediator, Interpreter)
(d) Which pattern prohibits multiple instances?
The Singleton pattern — it ensures a class has only one instance by making the constructor private and providing a single static access point (getInstance()).
(e) True or False: The State pattern encapsulates the behavior for each state in an object.
True. The State pattern encapsulates the behavior for each state in a dedicated class (state object). The context object delegates all state-dependent actions to the current state object, and behavior changes by swapping the state object.
(f) When to use Prototype, and what advantage does it provide?
Use Prototype when:
- The type of object to create should be determined dynamically at runtime.
- Creating a new object from scratch is complex or expensive — cloning a pre-configured prototype is cheaper.
- You want to avoid coupling client code to concrete classes.
Advantage: You can create new objects without knowing their concrete class — just call clone() on a prototype. You can also eliminate repeated initialization by keeping ready-made prototype objects.
(g) Differences between Builder and Prototype:
| Prototype | Builder | |
|---|---|---|
| Purpose | Copy an existing configured object | Assemble a new object step by step |
| Input | An existing prototype instance | A sequence of construction method calls |
| Result | A copy with the same structure/fields as the original | A new object possibly very different from any prior object |
| Best for | When initialization is expensive and objects share structure | When objects have many optional parts or multiple valid representations |
| Key mechanism | clone() / copy constructor |
buildStepX() methods + build() / getResult() |
Answer: All answers provided above.
3.2. Smart Document Editor (Lab 8, Task 2)
Implement a “Smart Document Editor” system in C++ demonstrating Singleton, State, and Prototype patterns working together.
Requirements:
- Part 1 — Singleton Logger: A
Loggerclass ensuring only one instance exists. Provideslog(const std::string& message)printing to the console. - Part 2 — State Pattern: An abstract
DocumentStateclass withvirtual void handleInput(const std::string& input). Concrete states:DraftState,ReviewState,FinalState. ADocumentclass holding aDocumentState*withchangeState(DocumentState* newState). - Part 3 — Prototype Pattern: An abstract
DocumentPrototypeclass withclone(). Concrete classes:ReportTypeandInvoiceType. Demonstrate cloning and log cloning events via the Singleton Logger. - Part 4 — Main function: Show document creation, state transitions, and cloning with all significant events logged.
Click to see the solution
Key Concept: Each pattern plays a distinct role: Singleton centralizes logging, State manages document lifecycle without if-chains, Prototype enables cheap document creation from templates.
#include <iostream>
#include <string>
// ===== PART 1: Singleton Logger =====
class Logger {
private:
static Logger* instance;
Logger() {} // private constructor
public:
static Logger* getInstance() {
if (instance == nullptr) {
instance = new Logger();
}
return instance;
}
void log(const std::string& message) {
std::cout << "[LOG] " << message << std::endl;
}
};
Logger* Logger::instance = nullptr;
// ===== PART 2: State Pattern =====
// Forward declaration needed because states reference Document
class Document;
// Abstract state
class DocumentState {
public:
virtual void handleInput(Document* doc, const std::string& input) = 0;
virtual std::string getName() const = 0;
virtual ~DocumentState() = default;
};
// Document class — holds current state, delegates behavior to it
class Document {
private:
DocumentState* state;
public:
Document(DocumentState* initialState) : state(initialState) {}
void changeState(DocumentState* newState) {
Logger::getInstance()->log(
"State changed from " + state->getName() +
" to " + newState->getName()
);
delete state;
state = newState;
}
void handleInput(const std::string& input) {
state->handleInput(this, input);
}
std::string getStateName() const { return state->getName(); }
~Document() { delete state; }
};
// Concrete states
class DraftState : public DocumentState {
public:
void handleInput(Document* doc, const std::string& input) override {
Logger::getInstance()->log("DraftState: received input '" + input + "'");
}
std::string getName() const override { return "Draft"; }
};
class ReviewState : public DocumentState {
public:
void handleInput(Document* doc, const std::string& input) override {
Logger::getInstance()->log("ReviewState: reviewing '" + input + "'");
}
std::string getName() const override { return "Review"; }
};
class FinalState : public DocumentState {
public:
void handleInput(Document* doc, const std::string& input) override {
Logger::getInstance()->log(
"FinalState: document is finalized. No edits allowed.");
}
std::string getName() const override { return "Final"; }
};
// ===== PART 3: Prototype Pattern =====
class DocumentPrototype {
public:
virtual DocumentPrototype* clone() const = 0;
virtual void describe() const = 0;
virtual ~DocumentPrototype() = default;
};
class ReportType : public DocumentPrototype {
std::string defaultHeader;
public:
ReportType(const std::string& header = "Default Report Header")
: defaultHeader(header) {}
DocumentPrototype* clone() const override {
Logger::getInstance()->log("Cloning ReportType prototype");
return new ReportType(*this); // copy constructor
}
void describe() const override {
std::cout << "ReportType | header: " << defaultHeader << std::endl;
}
};
class InvoiceType : public DocumentPrototype {
std::string defaultFooter;
public:
InvoiceType(const std::string& footer = "Default Invoice Footer")
: defaultFooter(footer) {}
DocumentPrototype* clone() const override {
Logger::getInstance()->log("Cloning InvoiceType prototype");
return new InvoiceType(*this);
}
void describe() const override {
std::cout << "InvoiceType | footer: " << defaultFooter << std::endl;
}
};
// ===== PART 4: Main =====
int main() {
Logger* logger = Logger::getInstance();
logger->log("System started");
// --- Prototype: create prototypes and clone them ---
ReportType* reportProto = new ReportType("Q1 Financial Report");
InvoiceType* invoiceProto = new InvoiceType("Invoice #2026-001");
DocumentPrototype* report1 = reportProto->clone();
DocumentPrototype* invoice1 = invoiceProto->clone();
report1->describe();
invoice1->describe();
// --- State: demonstrate state transitions ---
Document doc(new DraftState());
doc.handleInput("initial text"); // DraftState handles it
doc.changeState(new ReviewState());
doc.handleInput("review comment"); // ReviewState handles it
doc.changeState(new FinalState());
doc.handleInput("attempt to edit"); // FinalState: no edits allowed
// Cleanup
delete report1; delete invoice1;
delete reportProto; delete invoiceProto;
logger->log("System shut down");
return 0;
}Expected output:
[LOG] System started
[LOG] Cloning ReportType prototype
[LOG] Cloning InvoiceType prototype
ReportType | header: Q1 Financial Report
InvoiceType | footer: Invoice #2026-001
[LOG] DraftState: received input 'initial text'
[LOG] State changed from Draft to Review
[LOG] ReviewState: reviewing 'review comment'
[LOG] State changed from Review to Final
[LOG] FinalState: document is finalized. No edits allowed.
[LOG] System shut down
Answer: Singleton Logger provides one unified log stream. State pattern enables clean transitions — Document has no if-chains. Prototype pattern creates documents from pre-configured templates with a single clone() call.
3.3. Document Builder (Lab 8, Task 3)
Implement the Builder pattern in C++ for a Document with header, body, and footer sections. Include:
- A
Documentclass with three privatestd::stringmembers, setters, andprint(). - An abstract
DocumentBuilderclass with pure virtual functions for building each section. - A concrete
ReportBuilderimplementing the abstract class with report-specific content. - A
Directorclass with amake()function that usesReportBuilderto incrementally construct the document. - A
main()function demonstrating the system.
Click to see the solution
Key Concept: The Director orchestrates the build sequence; the ConcreteBuilder supplies the content. The client sees only the final product — construction logic is fully isolated.
#include <iostream>
#include <string>
// ===== Product =====
class Document {
private:
std::string header;
std::string body;
std::string footer;
public:
void setHeader(const std::string& h) { header = h; }
void setBody(const std::string& b) { body = b; }
void setFooter(const std::string& f) { footer = f; }
void print() const {
std::cout << "=== DOCUMENT ===" << std::endl;
std::cout << "Header: " << header << std::endl;
std::cout << "Body: " << body << std::endl;
std::cout << "Footer: " << footer << std::endl;
std::cout << "================" << std::endl;
}
};
// ===== Abstract Builder =====
class DocumentBuilder {
protected:
Document doc; // the product being built
public:
virtual ~DocumentBuilder() = default;
virtual void buildHeader() = 0;
virtual void buildBody() = 0;
virtual void buildFooter() = 0;
Document getResult() { return doc; }
};
// ===== Concrete Builder =====
class ReportBuilder : public DocumentBuilder {
public:
void buildHeader() override {
doc.setHeader("Quarterly Financial Report — Q1 2026");
}
void buildBody() override {
doc.setBody(
"Total Revenue: $1,250,000 | "
"Total Expenses: $870,000 | "
"Net Profit: $380,000"
);
}
void buildFooter() override {
doc.setFooter("Prepared by Finance Dept. | Confidential");
}
};
// ===== Director =====
class Director {
public:
// make() encapsulates the correct construction order
Document make(DocumentBuilder& builder) {
builder.buildHeader();
builder.buildBody();
builder.buildFooter();
return builder.getResult();
}
};
// ===== Main =====
int main() {
Director director;
ReportBuilder reportBuilder;
Document report = director.make(reportBuilder);
report.print();
return 0;
}Expected output:
=== DOCUMENT ===
Header: Quarterly Financial Report — Q1 2026
Body: Total Revenue: $1,250,000 | Total Expenses: $870,000 | Net Profit: $380,000
Footer: Prepared by Finance Dept. | Confidential
================
Documentholds the data — knows nothing about how it was built.DocumentBuilderdeclares what steps exist and stores the product being built.ReportBuilderfills in report-specific content for each step.Director.make()calls steps in the correct order. A different ConcreteBuilder (e.g.,InvoiceBuilder) could produce a completely different document using the same Director.- The client only interacts with the Director and receives the finished
Document.
Answer: The Builder pattern cleanly separates what a document is (Document), how to build it (ReportBuilder), and in what order (Director).
3.4. Implement the Singleton Pattern in C# (Lecture 8, Example 1)
Implement the Singleton design pattern in C#.
Click to see the solution
Key Concept: In C#, the pattern is essentially identical to Java: a private static field, a private constructor, and a public static Instance property.
public class Singleton
{
// private static field — null by default
private static Singleton unique;
// private constructor — prevents external instantiation
private Singleton() { }
// public static accessor with lazy initialization
public static Singleton Instance
{
get
{
if (unique == null)
{
// Note: for multithreaded apps, place a thread lock here
unique = new Singleton();
}
return unique;
}
}
}
// Usage
class Program
{
static void Main()
{
Singleton s1 = Singleton.Instance;
Singleton s2 = Singleton.Instance;
Console.WriteLine(s1 == s2); // True — same object
}
}- Private static field:
private static Singleton unique;— holds the one instance (initiallynull). - Private constructor: prevents any external
new Singleton()call. - Public property with lazy check: on first access,
uniqueisnull, so the instance is created. On all subsequent accesses, the same instance is returned. - Verification:
s1 == s2istruebecause both reference the same object.
Answer: The C# Singleton uses a private constructor, a private static field, and a public static property with a null-check to enforce a single instance.
3.5. Lazy vs. Non-Lazy Singleton: Compare the Implementations (Lecture 8, Example 2)
Given the two Singleton implementations below, explain the difference between them and state which is preferred and why.
Implementation A (Lazy):
public class LazySingleton {
private static LazySingleton unique;
private LazySingleton() { }
public static LazySingleton getInstance() {
if (unique == null) {
unique = new LazySingleton();
}
return unique;
}
}Implementation B (Non-Lazy):
public class NonLazySingleton {
private static final NonLazySingleton unique = new NonLazySingleton();
private NonLazySingleton() { }
public static NonLazySingleton getInstance() {
return unique;
}
}Click to see the solution
Key Concept: The difference is when the single instance is created — at first use (lazy) vs. at class loading time (non-lazy/eager).
| Lazy (A) | Non-Lazy (B) | |
|---|---|---|
| When created | On first call to getInstance() |
When the class is loaded by the JVM |
| Field declaration | private static LazySingleton unique; (null initially) |
private static final NonLazySingleton unique = new ... |
| Memory usage | Only if/when needed | Always, even if never used |
getInstance() body |
Null-check + possible construction | Simply return unique |
| Thread safety | Requires synchronization | JVM class loading is thread-safe by default |
| Preferred? | ✅ Generally preferred | ❌ Better to avoid |
Why Lazy is preferred: If the Singleton is never actually used in a run of the program, the Non-Lazy version wastes memory and initialization time. The Lazy version avoids this by creating the instance only on demand.
Why Non-Lazy is problematic: The instance is created eagerly regardless of need. Also, if the constructor has side effects (file I/O, network, etc.), they happen unconditionally at startup.
Answer: Implementation A (Lazy) is preferred because it creates the instance only when first requested, saving resources if the Singleton is never needed. Implementation B (Non-Lazy) should be avoided.
3.6. Implement the Singleton Pattern in C++ (Lecture 8, Example 3)
Implement the Singleton design pattern in C++.
Click to see the solution
Key Concept: In C++, the static field must be defined outside the class body. The rest of the pattern is the same as in Java/C#.
#include <iostream>
class Singleton {
private:
// declared inside the class (declaration only)
static Singleton* instance;
Singleton() { } // private constructor
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
void doSomething() {
std::cout << "Singleton instance in use" << std::endl;
}
};
// Required: define the static member outside the class (actual memory allocation)
Singleton* Singleton::instance = nullptr;
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
s1->doSomething();
std::cout << (s1 == s2 ? "Same instance" : "Different instances") << std::endl;
// Output: Same instance
return 0;
}- Declaration vs. definition: In C++,
static Singleton* instance;inside the class is only a declaration. The actual memory allocation happens inSingleton* Singleton::instance = nullptr;outside the class. - Lazy check:
if (instance == nullptr)— only the very first call allocates memory. - Private constructor: prevents
new Singleton()from being called outside the class.
Answer: The C++ Singleton requires an additional out-of-class definition of the static member pointer. The logic is otherwise identical to Java.
3.7. Prototype Pattern: Shapes Demo Trace (Lecture 8, Example 4)
Given the Shape, Circle, and Rectangle classes from the tutorial, trace through the Demo.main() method and predict what gets printed and why.
Click to see the solution
Key Concept: clone() creates a new object via the copy constructor — same field values, but different object identity (!=). The equals() method checks field values.
Setup (shapes list):
- A
Circlewithx=10, y=20, radius=15, color="red"— added toshapes. anotherCircle = (Circle) circle.clone()— a secondCirclewith identical fields — added toshapes.- A
Rectanglewithwidth=10, height=20, color="blue"— added toshapes.
Then cloneAndCompare() clones all three shapes into shapesCopy.
For each pair (shapes.get(i), shapesCopy.get(i)):
shapes.get(i) != shapesCopy.get(i)is always true —clone()always creates a new object at a different memory address; the reference!=test always passes.shapes.get(i).equals(shapesCopy.get(i))is true — the copy constructors faithfully copied all fields (x, y, color, and the subclass-specific fields), so content matches.
Expected output:
0: Shapes are different objects (yay!)
0: And their content are identical (yay!)
1: Shapes are different objects (yay!)
1: And their content are identical (yay!)
2: Shapes are different objects (yay!)
2: And their content are identical (yay!)
Answer: All three pairs print “different objects” (because clone() always allocates a new object) and “identical content” (because the copy constructors copied all fields correctly). This confirms the Prototype pattern is working.
3.8. Builder Pattern: Car Factory Trace (Lecture 8, Example 5)
Trace through Main.main() in the Java car builder example using SportsCarBuilder and predict the complete console output.
Click to see the solution
Key Concept: The Director (AutomotiveEngineer) calls the builder’s steps in a fixed sequence. Each step prints a message and sets a field. Method chaining means each call returns this (the same builder object).
Execution trace:
CarBuilder builder = new SportsCarBuilder();
AutomotiveEngineer eng = new AutomotiveEngineer(builder);
// AutomotiveEngineer constructor checks builder != null — OK
Car car = eng.manufactureCar();
// Calls: builder.fixChassis().fixBody().paint().fixInterior().build()Step by step:
builder.fixChassis()→ prints"Assembling chassis of the sports model", setschassis = "Sporty Chassis", returnsthis..fixBody()→ prints"Assembling body of the sports model", setsbody = "Sporty Body", returnsthis..paint()→ prints"Painting body of the sports model", setspaint = "Sporty Torch Red Paint", returnsthis..fixInterior()→ prints"Setting up interior of the sports model", setsinterior = "Sporty interior", returnsthis..build()→ createsCar("Sporty Chassis", "Sporty Body", "Sporty Torch Red Paint", "Sporty interior"), callsdoQualityCheck()— all 4 fields are non-null and non-empty → returnstrue. Returns the car.
car != null, so the main function prints:
Expected output:
Assembling chassis of the sports model
Assembling body of the sports model
Painting body of the sports model
Setting up interior of the sports model
Below car delivered:
============================================================================
Car [chassis=Sporty Chassis, body=Sporty Body, paint=Sporty Torch Red Paint]
============================================================================
If ClassicCarBuilder were used instead: The same sequence of prints would occur, but each line would say “classical model” instead of “sports model”, and the final car would show “Classic Chassis”, “Classic Body”, “Classic White Paint”. Same Director, different ConcreteBuilder, different product — this is the core value of the Builder pattern.
Answer: Each step prints its assembly message in sequence; build() verifies quality and returns the complete car. Swapping SportsCarBuilder for ClassicCarBuilder produces a different car with no changes to the Director.
3.9. Thread Safety in Singleton (Lecture 8, Example 6)
(a) Explain why the basic Singleton implementation is incomplete. (b) Illustrate the race condition. (c) Propose an industrial-strength solution.
Click to see the solution
Key Concept: In a multithreaded environment, two threads can simultaneously pass the null check and both create a new instance, breaking the uniqueness guarantee.
(a) Why the basic implementation is incomplete:
Consider two threads T1 and T2 executing getInstance() simultaneously:
T1: checks unique == null → true (instance not yet created)
T2: checks unique == null → true (T1 hasn't finished — unique is still null)
T1: creates new Singleton() → assigns to unique
T2: creates new Singleton() → overwrites unique with a SECOND instance!
Both threads passed the null-check before either finished creating the object. Result: two Singleton instances — the contract is broken.
(b) Multithreaded illustration:
public class Singleton {
private static Singleton unique;
private Singleton() { }
// NOT thread-safe — race condition possible
public static Singleton getInstance() {
if (unique == null) {
// Both threads can reach this line at the same time
unique = new Singleton();
}
return unique;
}
}(c) Industrial-strength solutions:
Solution 1 — Synchronized method (simple but adds overhead on every call):
public static synchronized Singleton getInstance() {
if (unique == null) {
unique = new Singleton();
}
return unique;
}synchronized locks the method so only one thread executes it at a time. Downside: every call acquires a lock, even after the instance is already created.
Solution 2 — Double-checked locking (fast and correct):
public class Singleton {
// volatile ensures changes are visible across threads immediately
private static volatile Singleton unique;
private Singleton() { }
public static Singleton getInstance() {
if (unique == null) { // First check — no lock (fast path)
synchronized (Singleton.class) {
if (unique == null) { // Second check — with lock (safe)
unique = new Singleton();
}
}
}
return unique;
}
}The outer check avoids locking on every call. Only the first time (when unique is null) does the code acquire the lock, and the inner check guards against the race.
Answer: The basic Singleton is not thread-safe. Fix: use synchronized (simple) or double-checked locking with volatile (efficient).